// SPDX-FileCopyrightText: Adam Evyčędo
//
// SPDX-License-Identifier: GPL-3.0-or-later

package xyz.apiote.bimba.czwek.dashboard.ui.map

import android.annotation.SuppressLint
import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
import android.content.res.Configuration.*
import android.graphics.Bitmap
import android.net.ConnectivityManager
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.edit
import androidx.core.graphics.drawable.toBitmap
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.snackbar.Snackbar
import org.osmdroid.config.Configuration
import org.osmdroid.events.MapListener
import org.osmdroid.events.ScrollEvent
import org.osmdroid.events.ZoomEvent
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.CustomZoomButtonsController
import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.TilesOverlay
import org.osmdroid.views.overlay.gestures.RotationGestureOverlay
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
import xyz.apiote.bimba.czwek.R
import xyz.apiote.bimba.czwek.dashboard.MainActivity
import xyz.apiote.bimba.czwek.databinding.FragmentMapBinding
import xyz.apiote.bimba.czwek.dpToPixelI
import xyz.apiote.bimba.czwek.repo.ErrorLocatable
import xyz.apiote.bimba.czwek.repo.Position
import xyz.apiote.bimba.czwek.repo.Stop
import xyz.apiote.bimba.czwek.repo.Vehicle

class MapFragment : Fragment() {

	private var maybeBinding: FragmentMapBinding? = null
	private val binding get() = maybeBinding!!

	private lateinit var locationOverlay: MyLocationNewOverlay
	private lateinit var mapViewModel: MapViewModel

	private val handler = Handler(Looper.getMainLooper())
	private var workRunnable = Runnable {}

	private var snack: Snackbar? = null

	@SuppressLint("ClickableViewAccessibility")
	override fun onCreateView(
		inflater: LayoutInflater,
		container: ViewGroup?,
		savedInstanceState: Bundle?
	): View {
		mapViewModel =
			ViewModelProvider(this)[MapViewModel::class.java]

		observeLocatables()

		maybeBinding = FragmentMapBinding.inflate(inflater, container, false)
		val root: View = binding.root

		binding.map.setTileSource(TileSourceFactory.MAPNIK)
		if (((context?.resources?.configuration?.uiMode ?: UI_MODE_NIGHT_UNDEFINED)
							and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES
		) {
			binding.map.overlayManager.tilesOverlay.setColorFilter(TilesOverlay.INVERT_COLORS)
		}
		binding.map.zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
		binding.map.setMultiTouchControls(true)
		binding.map.overlays.add(RotationGestureOverlay(binding.map).apply { isEnabled = true })

		locationOverlay = MyLocationNewOverlay(GpsMyLocationProvider(context), binding.map)
		context?.let {
			centreMap(it.getSharedPreferences("shp", MODE_PRIVATE))

			locationOverlay.setDirectionIcon(
				AppCompatResources.getDrawable(it, R.drawable.navigation_arrow)?.mutate()
					?.toBitmap(dpToPixelI(36f), dpToPixelI(36f), Bitmap.Config.ARGB_8888)
			)
			locationOverlay.setDirectionAnchor(.5f, .5f)
			locationOverlay.setPersonIcon(
				AppCompatResources.getDrawable(it, R.drawable.navigation_circle)?.mutate()
					?.toBitmap(dpToPixelI(24f), dpToPixelI(24f), Bitmap.Config.ARGB_8888)
			)
			locationOverlay.setPersonAnchor(.5f, .5f)
		}

		binding.floatingActionButton.setOnClickListener {
			(context as MainActivity).onGpsClicked(this)
		}

		binding.map.addMapListener(object : MapListener {
			override fun onScroll(event: ScrollEvent?): Boolean {
				return onMapMove()
			}

			override fun onZoom(event: ZoomEvent?): Boolean {
				return onMapMove()
			}
		})

		binding.map.setOnTouchListener { _, _ ->
			binding.floatingActionButton.show()
			false
		}

		return root
	}

	private fun onMapMove(): Boolean {
		snack?.dismiss()
		return delayGetLocatables()
	}

	private fun delayGetLocatables(delay: Long = 1000): Boolean {
		handler.removeCallbacks(workRunnable)
		workRunnable = Runnable {
			getLocatables()
		}
		handler.postDelayed(workRunnable, delay)
		return true
	}

	private fun observeLocatables() {
		mapViewModel.locatables.observe(viewLifecycleOwner) {
			binding.map.overlays.removeAll { marker ->
				marker is Marker
			}

			if (it.size == 1 && it[0] is ErrorLocatable) {
				Snackbar.make(binding.root, (it[0] as ErrorLocatable).stringResource, Snackbar.LENGTH_LONG)
					.show()
				return@observe
			}

			it.forEach { locatable ->
				val marker = Marker(binding.map)
				marker.position = GeoPoint(locatable.location().latitude, locatable.location().longitude)
				when (locatable) {
					is Stop -> marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
					is Vehicle -> marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
				}

				val scale = binding.map.zoomLevelDouble / -4 + 5.5
				marker.icon = context?.let { ctx -> locatable.icon(ctx, scale.toFloat()) }

				context?.let { ctx ->
					marker.setOnMarkerClickListener { _, _ ->
						MapBottomSheet(locatable).apply {
							(ctx as MainActivity?)?.supportFragmentManager?.let { fm ->
								show(fm, MapBottomSheet.TAG)
							}
						}
						true
					}
				}
				binding.map.overlays.add(marker)
			}

			binding.map.invalidate()
		}
	}

	fun showLocation() {
		snack =
			Snackbar.make(binding.root, getString(R.string.waiting_position), Snackbar.LENGTH_INDEFINITE)
		snack!!.show()
		binding.floatingActionButton.hide()
		binding.map.overlays.removeAll {
			it is MyLocationNewOverlay
		}
		locationOverlay.enableFollowLocation()
		binding.map.overlays.add(locationOverlay)
		locationOverlay.runOnFirstFix {
			snack?.dismiss()
		}
	}

	private fun getLocatables() {
		maybeBinding?.let { binding ->
			val (bl, tr) = binding.map.boundingBox.let {
				Pair(
					Position(it.latSouth, it.lonWest),
					Position(it.latNorth, it.lonEast)
				)
			}
			context?.let {
				val cm = it.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
				mapViewModel.getLocatablesIn(cm, bl, tr, it)
			}
			delayGetLocatables(30000)
		}
	}

	private fun centreMap(preferences: SharedPreferences) {
		maybeBinding?.map?.controller?.apply {
			setZoom(preferences.getFloat("mapZoom", 17.0f).toDouble())
			val startPoint = GeoPoint(
				preferences.getFloat("mapCentreLat", 52.39511f).toDouble(),
				preferences.getFloat("mapCentreLon", 16.89506f).toDouble()
			)
			setCenter(startPoint)
		}
	}

	override fun onResume() {
		super.onResume()
		binding.map.onResume()
		locationOverlay.enableMyLocation()
		context?.let { ctx ->
			ctx.getSharedPreferences("shp", MODE_PRIVATE).let {
				Configuration.getInstance()
					.load(ctx, it)
				centreMap(it)
			}
		}
	}

	override fun onPause() {
		super.onPause()
		binding.map.onPause()
		locationOverlay.disableMyLocation()
		val centre = binding.map.mapCenter
		context?.let { ctx ->
			ctx.getSharedPreferences("shp", MODE_PRIVATE).edit(true) {
				this.putFloat("mapCentreLat", centre.latitude.toFloat())
				this.putFloat("mapCentreLon", centre.longitude.toFloat())
				this.putFloat("mapZoom", binding.map.zoomLevelDouble.toFloat())
			}
		}
		handler.removeCallbacks(workRunnable)
	}

	override fun onDestroyView() {
		super.onDestroyView()
		maybeBinding = null
	}
}